/*
 * Copyright (c) 2007 Aleksey Nikiforov
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'Aleksey Nikiforov' nor the names of other contributors 
 *   may be used to endorse or promote products derived from this software 
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.lex.input.mouse.component;


import org.lex.input.mouse.AnotherAbsoluteMouse;
import org.lex.input.mouse.Cursor;

import com.jme.input.InputHandler;
import com.jme.input.action.InputAction;
import com.jme.input.action.InputActionEvent;
import com.jme.math.Vector2f;
import com.jme.renderer.Renderer;
import com.jme.scene.Spatial;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.TextureState;

/**
 * Software mouse implements AdvancedMouse with an addition of
 * some extra methods. All the methods are always guaranteed to secceed.
 * 
 * @author lex
 *
 */
public class SoftwareMouse implements AdvancedMouse {

	private boolean visible;
	
	private AnotherAbsoluteMouse mouse;
	private Vector2f absolute;
	private float sensitivity;
	
	private Cursor cursor;
	private TextureState textureState;
	
	private InputHandler currentHandler;
	
	/**
	 * While it is not absolutely nessesary to keep absolute coordinates
	 * separately it helps us confor to the standart of returning
	 * Vector2f as hotSpotPosition rather than Vector3f returned by
	 * the absolute mouse.
	 */
	private InputAction updateAction = new InputAction() {
		public void performAction(InputActionEvent evt) {
			if (visible) {
				cursor.update(evt.getTime());
				textureState.setTexture(cursor.getNextTextureFrame());
			}
		}
	};
	
	/**
	 * This constructor will create a SoftwareMouse in a disabled state
	 * (the mouse will be inactive and invisible).
	 * 
	 * @param name
	 */
	public SoftwareMouse(String name, Renderer renderer) {
		mouse = new AnotherAbsoluteMouse(name,
				renderer.getWidth(), renderer.getHeight());
		absolute = mouse.getMousePosition();
		
		textureState = renderer.createTextureState();
		textureState.setEnabled(true);
		setCursor(defaultCursor);
		mouse.setCullHint(Spatial.CullHint.Always);
		mouse.setZOrder(Integer.MIN_VALUE + 4);
		
		BlendState as = renderer.createBlendState();
		as.setBlendEnabled(true);
		as.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
		as.setDestinationFunction(BlendState.DestinationFunction.OneMinusSourceAlpha);
		as.setTestEnabled(true);
		as.setTestFunction(BlendState.TestFunction.GreaterThan);
		as.setEnabled(true);
		mouse.setRenderState(as);
		
		mouse.updateRenderState();
		
		sensitivity = mouse.getSpeed();
	}
	
	/**
	 * Call this after changing display settings.
	 */
	public void reinit(int width, int height) {
		mouse.resize(width, height);
	}
	
	public float getSensitivity() {
		return sensitivity;
	}
	
	/**
	 * It is recommended to use this method in favor of
	 * absoluteMouse.setSpeed().
	 * @param sensitivity
	 */
	public void setSensitivity(float sensitivity) {
		this.sensitivity = sensitivity;
		mouse.setSpeed(sensitivity);
	}
	
	/**
	 * You must attach this spatial to a node that will be rendered, otherwise
	 * cursor will not be visible.
	 * @return spatial to attached to some node (e.g. rootNode) for rendering
	 */
	public Spatial getMouseSpatial() {
		return mouse;
	}
	
	public void registerWithInputHandler(InputHandler handler) {
        if (currentHandler != null) {
            currentHandler.removeAction(updateAction);
        }
		
        if (handler != null) {
        	handler.addAction(updateAction,
            	InputHandler.DEVICE_MOUSE, InputHandler.BUTTON_NONE, 0, true);
        }
        
        currentHandler = handler;
        
       	mouse.registerWithInputHandler(handler);
	}
	
	/**
	 * @see net.mindgamer.risetothestars.common.mousemanager.component.BasicMouse#enable()
	 */
	public void enable() {
		setActive(true);
		setCursorVisible(true);
	}

	/**
	 * @see net.mindgamer.risetothestars.common.mousemanager.component.BasicMouse#disable()
	 */
	public void disable() {
		setActive(false);
		setCursorVisible(false);
	}
	
	public boolean isEnabled() {
		return (isActive() && visible);
	}
	
	/**
	 * @see net.mindgamer.risetothestars.common.mousemanager.component.AdvancedMouse#isActive()
	 */
	public boolean isActive() {
		return mouse.isActive();
	}
	
	/**
	 * @see net.mindgamer.risetothestars.common.mousemanager.component.AdvancedMouse#isCursorVisible()
	 */
	public boolean isCursorVisible() {
		return visible;
	}
	
	/**
	 * @see net.mindgamer.risetothestars.common.mousemanager.component.AdvancedMouse#setActive(boolean)
	 */
	public void setActive(boolean active) {
		if (active == isActive()) return;
		
		mouse.setActive(active);
	}
	
	/**
	 * setCursorVisible(true) will have no effect if the null cursor is set.
	 * @see net.mindgamer.risetothestars.common.mousemanager.component.AdvancedMouse#setCursorVisible(boolean)
	 */
	public void setCursorVisible(boolean visible) {
		if (visible == this.visible) return;
		
		if (visible) {
			resetAnimation();
			mouse.setCullHint(Spatial.CullHint.Never);
		} else {
			mouse.setCullHint(Spatial.CullHint.Always);
		}
		
		this.visible = visible;
	}
	
	/**
	 * Setting the null cursor will cause the cursor to disappear.
	 * @see net.mindgamer.risetothestars.common.mousemanager.component.BasicMouse#setCursor(net.mindgamer.risetothestars.common.mousemanager.Cursor)
	 */
	public void setCursor(Cursor newCursor) {
		if (newCursor != null) cursor = newCursor;
		else cursor = defaultCursor;
		
		resetAnimation();
		mouse.setRenderState(textureState);
		
		Vector2f offset = cursor.getHotSpotOffset();
		mouse.getHotSpotOffset().set(
				offset.x - cursor.getWidth()/2f,
				offset.y - cursor.getHeight()/2f,
				0);
		
		// reposition the mouse according to the new hotSpotOffset
		mouse.setHotSpotPosition(absolute.x, absolute.y);
	}
	
	public void resetAnimation() {
		cursor.restartAnimation();
		textureState.setTexture(cursor.getNextTextureFrame());
	}
	
	public Cursor getCursor() {
		return (cursor == defaultCursor ? null : cursor);
	}
	
	public void setHotSpotPosition(float x, float y) {
		mouse.setHotSpotPosition(x, y);
	}
	
	public void setHotSpotPosition(Vector2f position) {
		mouse.setHotSpotPosition(position.x, position.y);
	}

	public Vector2f getHotSpotPosition() {
		return absolute;
	}

	public float getHeight() {
		return mouse.getHeight();
	}

	public float getWidth() {
		return mouse.getWidth();
	}

}